package mylab.starters.oss.support.minio;

import io.minio.*;
import io.minio.messages.DeleteObject;
import lombok.SneakyThrows;
import mylab.starters.oss.AbstractOssTemplate;
import mylab.starters.oss.OssProvider;
import mylab.starters.oss.OssRule;
import mylab.starters.oss.config.OssProperties;
import mylab.starters.oss.model.OssFile;
import mylab.starters.oss.model.OssFileMetaInfo;
import mylab.starters.oss.support.minio.enums.PolicyType;
import mylab.utils.common.MimeTypeUtil;
import mylab.utils.common.StrUtil;
import mylab.utils.spring.spi.SpiProvider;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 * MinIO 操作，参考文档：https://docs.min.io/docs/java-client-api-reference.html
 * </p>
 */
public class MinioTemplate extends AbstractOssTemplate<OssProperties.MinioProperties> {
    private final MinioClient minioClient;

    public MinioTemplate(OssProperties.MinioProperties properties, OssRule ossRule, MinioClient minioClient) {
        super(properties, ossRule);
        this.minioClient = minioClient;
    }

    @Override
    public SpiProvider getProvider() {
        return OssProvider.Minio;
    }

    /**
     * 创建 存储桶
     *
     * @param bucketName 存储桶名称
     */
    @Override
    @SneakyThrows
    public void createBucket(String bucketName) {
        bucketName = getBucketName(bucketName);
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());

            String policyType = getPolicyType(bucketName, PolicyType.READ);
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(policyType).build());
        }
    }

    /**
     * 删除 存储桶
     *
     * @param bucketName 存储桶名称
     */
    @Override
    @SneakyThrows
    public void deleteBucket(String bucketName) {
        if (bucketExists(bucketName)) {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(getBucketName(bucketName)).build());
        }
    }

    /**
     * 存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return boolean
     */
    @Override
    @SneakyThrows
    public boolean bucketExists(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder()
                .bucket(getBucketName(bucketName)).build());
    }

    /**
     * 拷贝文件
     *
     * @param sourceBucketName 源存储桶名称
     * @param fileName         存储桶文件名称
     * @param targetBucketName 目标存储桶名称
     */
    @Override
    public void copyFile(String sourceBucketName, String fileName, String targetBucketName) {
        copyFile(sourceBucketName, fileName, targetBucketName, fileName);
    }

    /**
     * 拷贝文件，重命名
     *
     * @param sourceBucketName 源存储桶名称
     * @param fileName         存储桶文件名称
     * @param targetBucketName 目标存储桶名称
     * @param targetFileName   目标存储桶文件名称
     */
    @Override
    @SneakyThrows
    public void copyFile(String sourceBucketName, String fileName, String targetBucketName, String targetFileName) {
        String source = getBucketName(sourceBucketName);
        String target = getBucketName(targetBucketName);
        minioClient.copyObject(CopyObjectArgs.builder()
                .source(CopySource.builder().bucket(source).object(fileName).build())
                .bucket(target).object(targetFileName)
                .build()
        );
    }

    /**
     * 获取文件元信息
     *
     * @param fileName 存储桶文件名称
     * @return 文件元信息
     */
    @Override
    public OssFileMetaInfo getFileMetaInfo(String fileName) {
        return getFileMetaInfo(properties.getBucketName(), fileName);
    }

    /**
     * 获取文件元信息
     *
     * @param bucketName 存储桶名称
     * @param fileName   存储桶文件名称
     * @return 文件元信息
     */
    @Override
    @SneakyThrows
    public OssFileMetaInfo getFileMetaInfo(String bucketName, String fileName) {
        StatObjectArgs args = StatObjectArgs.builder().bucket(bucketName).object(fileName).build();
        StatObjectResponse objectStat = minioClient.statObject(args);
        OssFileMetaInfo metaInfo = new OssFileMetaInfo();
        metaInfo.setName(StrUtil.isNotBlank(objectStat.object()) ? objectStat.object() : fileName);
        metaInfo.setLink(getFileLink(metaInfo.getName()));
        metaInfo.setHash(String.valueOf(objectStat.hashCode()));
        metaInfo.setLength(objectStat.size());
        metaInfo.setUploadTime(Date.from(objectStat.lastModified().toInstant()));
        metaInfo.setContentType(objectStat.contentType());
        return metaInfo;
    }

    /**
     * 获取文件相对路径
     *
     * @param fileName 存储桶对象名称
     * @return 文件相对路径
     */
    @Override
    public String getFilePath(String fileName) {
        return getBucketName().concat(StrUtil.SLASH)
                .concat(fileName);
    }

    /**
     * 获取文件相对路径
     *
     * @param bucketName 存储桶名称
     * @param fileName   存储桶对象名称
     * @return 文件相对路径
     */
    @Override
    public String getFilePath(String bucketName, String fileName) {
        return getBucketName(bucketName).concat(StrUtil.SLASH)
                .concat(fileName);
    }

    /**
     * 获取文件地址
     *
     * @param fileName 存储桶对象名称
     * @return 文件地址
     */
    @Override
    public String getFileLink(String fileName) {
        return properties
                .getEndpoint()
                .concat(StrUtil.SLASH)
                .concat(getBucketName())
                .concat(StrUtil.SLASH)
                .concat(fileName);
    }

    /**
     * 获取文件地址
     *
     * @param bucketName 存储桶名称
     * @param fileName   存储桶对象名称
     * @return 文件地址
     */
    @Override
    public String getFileLink(String bucketName, String fileName) {
        return properties
                .getEndpoint()
                .concat(StrUtil.SLASH)
                .concat(getBucketName(bucketName))
                .concat(StrUtil.SLASH)
                .concat(fileName);
    }

    /**
     * 上传文件
     *
     * @param file 上传文件类
     * @return 文件信息
     */
    @Override
    public OssFile uploadFile(MultipartFile file) {
        return uploadFile(file.getOriginalFilename(), file);
    }

    /**
     * 上传文件
     *
     * @param fileName 上传文件名
     * @param file     上传文件类
     * @return 文件信息
     */
    @Override
    public OssFile uploadFile(String fileName, MultipartFile file) {
        return uploadFile(properties
                .getBucketName(), fileName, file);
    }

    /**
     * 上传文件
     *
     * @param bucketName 存储桶名称
     * @param fileName   上传文件名
     * @param file       上传文件类
     * @return 文件信息
     */
    @Override
    @SneakyThrows
    public OssFile uploadFile(String bucketName, String fileName, MultipartFile file) {
        return uploadFile(bucketName, fileName, file.getInputStream());
    }

    /**
     * 上传文件
     *
     * @param fileName 存储桶对象名称
     * @param stream   文件流
     * @return 文件信息
     */
    @Override
    public OssFile uploadFile(String fileName, InputStream stream) {
        return uploadFile(properties
                .getBucketName(), fileName, stream);
    }

    /**
     * 上传文件
     *
     * @param bucketName 存储桶名称
     * @param fileName   存储桶对象名称
     * @param stream     文件流
     * @return 文件信息
     */
    @Override
    @SneakyThrows
    public OssFile uploadFile(String bucketName, String fileName, InputStream stream) {
        // 创建存储桶
        createBucket(bucketName);
        // 获取 oss 存储文件名
        String key = getFileName(fileName);

        long size = stream.available();

        minioClient.putObject(PutObjectArgs.builder()
                .bucket(getBucketName(bucketName))
                .object(key)
                .stream(stream, size, -1)
                .contentType(MimeTypeUtil.getContentTypeByFile(key))
                .build());

        OssFile ossFile = new OssFile();
        ossFile.setName(key);
        ossFile.setOriginalName(fileName);
        ossFile.setLink(getFileLink(bucketName, key));
        return ossFile;
    }

    /**
     * 删除文件
     *
     * @param fileName 存储桶对象名称
     */
    @Override
    @SneakyThrows
    public void deleteFile(String fileName) {
        minioClient.removeObject(RemoveObjectArgs.builder().bucket(getBucketName()).object(fileName).build());
    }

    /**
     * 删除文件
     *
     * @param bucketName 存储桶名称
     * @param fileName   存储桶对象名称
     */
    @Override
    @SneakyThrows
    public void deleteFile(String bucketName, String fileName) {
        minioClient.removeObject(RemoveObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build());
    }

    /**
     * 批量删除文件
     *
     * @param fileNames 存储桶对象名称集合
     */
    @Override
    public void deleteFiles(List<String> fileNames) {
        Iterable<DeleteObject> objects = fileNames.stream().map(DeleteObject::new).collect(Collectors.toList());
        minioClient.removeObjects(RemoveObjectsArgs.builder().objects(objects).build());
    }

    /**
     * 批量删除文件
     *
     * @param bucketName 存储桶名称
     * @param fileNames  存储桶对象名称集合
     */
    @Override
    public void deleteFiles(String bucketName, List<String> fileNames) {
        Iterable<DeleteObject> objects = fileNames.stream().map(DeleteObject::new).collect(Collectors.toList());
        minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(getBucketName(bucketName)).objects(objects).build());
    }

    /**
     * 获取存储桶策略
     *
     * @param policyType 策略枚举
     * @return 存储桶策略
     */
    public String getPolicyType(PolicyType policyType) {
        return getPolicyType(getBucketName(), policyType);
    }

    /**
     * 获取存储桶策略，参考 {@link MinioClient#setBucketPolicy(SetBucketPolicyArgs)}的注释
     *
     * @param bucketName 存储桶名称
     * @param policyType 策略枚举
     * @return 存储桶策略
     */
    public static String getPolicyType(String bucketName, PolicyType policyType) {
        StringBuilder builder = new StringBuilder();
        builder.append("{\n");
        builder.append("    \"Statement\": [\n");
        builder.append("        {\n");
        builder.append("            \"Action\": [\n");

        switch (policyType) {
            case WRITE:
                builder.append("                \"s3:GetBucketLocation\",\n");
                builder.append("                \"s3:ListBucketMultipartUploads\"\n");
                break;
            case READ_WRITE:
                builder.append("                \"s3:GetBucketLocation\",\n");
                builder.append("                \"s3:ListBucket\",\n");
                builder.append("                \"s3:ListBucketMultipartUploads\"\n");
                break;
            default:
                builder.append("                \"s3:GetBucketLocation\"\n");
                break;
        }

        builder.append("            ],\n");
        builder.append("            \"Effect\": \"Allow\",\n");
        builder.append("            \"Principal\": \"*\",\n");
        builder.append("            \"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("\"\n");
        builder.append("        },\n");
        if (PolicyType.READ.equals(policyType)) {
            builder.append("        {\n");
            builder.append("            \"Action\": [\n");
            builder.append("                \"s3:ListBucket\"\n");
            builder.append("            ],\n");
            builder.append("            \"Effect\": \"Deny\",\n");
            builder.append("            \"Principal\": \"*\",\n");
            builder.append("            \"Resource\": \"arn:aws:s3:::");
            builder.append(bucketName);
            builder.append("\"\n");
            builder.append("        },\n");
        }
        builder.append("        {\n");
        builder.append("            \"Action\": ");

        switch (policyType) {
            case WRITE:
                builder.append("[\n");
                builder.append("                \"s3:AbortMultipartUpload\",\n");
                builder.append("                \"s3:DeleteObject\",\n");
                builder.append("                \"s3:ListMultipartUploadParts\",\n");
                builder.append("                \"s3:PutObject\"\n");
                builder.append("            ],\n");
                break;
            case READ_WRITE:
                builder.append("[\n");
                builder.append("                \"s3:AbortMultipartUpload\",\n");
                builder.append("                \"s3:DeleteObject\",\n");
                builder.append("                \"s3:GetObject\",\n");
                builder.append("                \"s3:ListMultipartUploadParts\",\n");
                builder.append("                \"s3:PutObject\"\n");
                builder.append("            ],\n");
                break;
            default:
                builder.append("\"s3:GetObject\",\n");
                break;
        }

        builder.append("            \"Effect\": \"Allow\",\n");
        builder.append("            \"Principal\": \"*\",\n");
        builder.append("            \"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("/*\"\n");
        builder.append("        }\n");
        builder.append("    ],\n");
        builder.append("    \"Version\": \"2012-10-17\"\n");
        builder.append("}\n");
        return builder.toString();
    }
}
